package cz.possoft.explorer.meta;

import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import cz.possoft.explorer.meta.builder.DomainMetadataException;
import cz.possoft.explorer.resolver.Resolver;
import cz.possoft.explorer.resolver.method.MethodKey;
import cz.possoft.explorer.resolver.method.MethodResolver;

/**
 * This contains all metainformation about all different class objects, which are used by our framework.
 * 
 * @author <a href="mailto:mposolda@gmail.com">Marek Posolda</a>
 * @version $Revision$
 */
public class DomainMetadata
{
   // Content of this map is main state of DomainMetadata object and it is filled by DomainMetadataBuilder.
   // For each item, Class param type should be same as first type of ClassMetadata. But it seems that this can't be specified in Java :-/
   private Map<Class<?>, ClassMetadata<?, ?>> classMetadataMap= new HashMap<Class<?>, ClassMetadata<?,?>>();    
   
   /**
    * Add class and it's resolver to metadata map after building of ClassMetadata. Also add all supertypes classes
    * and their interfaces from this path to the root path.
    * 
    * @param <C>
    * @param clazz
    * @param resolver
    */   
   public <C,T> void addClassWithAllSupertypes(Class<C> clazz, Resolver<C,T> resolver, Class<? extends ClassMetadata<C,T>> classMetadataClass, MethodResolver<C,MethodKey> methodResolver) throws DomainMetadataException
   {            
      // STEP 1 - get superclass and interfaces      
      Class<? super C> superclass = clazz.getSuperclass();
      Class<?>[] interfaceClasses = clazz.getInterfaces();
      
      // STEP 2 - call this method for all interfaces first and then for supertype. Use recursion
      if (interfaceClasses != null)
      {
         for (Class<?> interfaceClass : interfaceClasses)
         {
            // TODO: we are using same resolver and classMetadataclass. This should be improved in future versions as well as type safety.
            addClassWithAllSupertypes((Class)interfaceClass, resolver, classMetadataClass, methodResolver);
         }         
      }
      if (superclass != null)
      {
         // TODO: we are using same resolver and classMetadataclass. This should be improved in future versions as well as type safety.
         addClassWithAllSupertypes((Class)superclass, resolver, classMetadataClass, methodResolver);
      }                        
      
      // STEP 3 - add this class
      addClass(clazz, resolver, classMetadataClass, methodResolver);
      
      // STEP 4 - get all registered superclasses and superinterfaces and retain all fields from them
      Collection<Class<? super C>> predecessors = getAllPredecessors(clazz);
      for (Class<? super C> superrClass : predecessors)
      {
         retainFields(clazz, superrClass);
      }
   }
   
   /**
    * Add class and it's resolver to metadata map after building of ClassMetadata
    * 
    * @param <C>
    * @param clazz
    * @param resolver
    */
   public <C,T> void addClass(Class<?> clazz, Resolver<C,T> resolver, Class<? extends ClassMetadata<C,T>> classMetadataClass, MethodResolver<C,MethodKey> methodResolver) throws DomainMetadataException
   {
      // nothing to do if clazz is already in map
      if (classMetadataMap.containsKey(clazz))
      {
         return;
      }

      ClassMetadata<C,T> metadata;
      
      // this can be null because of PropertyDomainMetadataBuilder
      if (classMetadataClass == null)
      {
         metadata = new ClassMetadata(clazz, resolver, methodResolver);
      }
      
      try
      {
         Constructor<? extends ClassMetadata<C,T>> constr = classMetadataClass.getConstructor(Class.class, Resolver.class, MethodResolver.class);
         metadata = constr.newInstance(clazz, resolver, methodResolver);
         classMetadataMap.put(clazz, metadata);
      }
      catch (Exception e)
      {
         throw new DomainMetadataException(e);
      }            
   }
   
   /**
    * 
    * @return list of all configured classes.
    */
   public Collection<Class<?>> getAllConfiguredClasses()
   {
      return Collections.unmodifiableCollection(classMetadataMap.keySet());
   }
   
   /**
    * FIXME Comment this
    * 
    * @param <C>
    * @param <T>
    * @param clazz
    * @return class metadata
    */
   public <C,T> ClassMetadata<C,T> getClassMetadata(Class<C> clazz)
   {
      // cast should be ok according to addClass method.
      return (ClassMetadata<C,T>)classMetadataMap.get(clazz);
   }
   
   /**
    * Remove all fields which are declared in interface class from the Class clazz.
    * So that we don't have duplicate fields in class.
    * 
    * @param clazz
    * @param interfaceClass
    */
   private <C, I, T> void retainFields(Class<C> clazz, Class<I> interfaceClass)
   {
      ClassMetadata<C,T> clazzMetadata = getClassMetadata(clazz);
      ClassMetadata<I,T> interfaceMetadata = getClassMetadata(interfaceClass);
      
      Map<T, Class<?>> clazzFields = clazzMetadata.getFields(null);
      Map<T, Class<?>> interfaceFields = interfaceMetadata.getFields(null);
      
      for (T fieldKey : interfaceFields.keySet())
      {
         clazzFields.remove(fieldKey);
      }
   }
   
   private static <Z> Collection<Class<? super Z>> getAllPredecessors(Class<Z> clazz)
   {
      // create result collection
      Set<Class<? super Z>> superclasses = new HashSet<Class<? super Z>>();
      
      // obtain superclass and declared interfaces
      Class<? super Z> superclass = clazz.getSuperclass();
      Class<?>[] interfaceClasses = clazz.getInterfaces();
      
      // Call this method for all interfaces. Use recursion.
      if (interfaceClasses != null)
      {
         for (Class<?> interfaceClass : interfaceClasses)
         {
            // this should be safe
            superclasses.add((Class<? super Z>)interfaceClass);            
            superclasses.addAll(getAllPredecessors((Class<? super Z>)interfaceClass));
         }         
      }
      
      // Call this method for superclass. Use recursion.
      if (superclass != null)
      {
         superclasses.add((Class<? super Z>)superclass);            
         superclasses.addAll(getAllPredecessors((Class<? super Z>)superclass));         
      }   
      
      return superclasses;
   }

}

